Popular Searches
Popular Course Categories
Popular Courses

Writing Unit Tests with JUnit

Writing Unit Tests with JUnit

Testing and Debugging Android Applications

Writing Unit Tests with JUnit in Android Development

Course: Android App Development

Topic: Writing Unit Tests with JUnit

Course Link: Android App Development Course

Demo Link: Register for a Free Course Demo

Website: JustAcademy


1. Introduction to Unit Testing in Android

Unit testing is a software testing technique used to verify that individual units of an application work correctly. A unit may be a method, function, class, or a small component that performs a specific task. In Android development, unit testing helps developers identify logical errors before an application is released to users.

JUnit is a popular Java testing framework used to write and execute automated tests. Android developers commonly use JUnit to test business logic, data processing, validation rules, calculations, and other components that can run independently of the Android user interface.

For example, if an Android application calculates the total price of products in a shopping cart, a JUnit test can verify that the calculation returns the correct amount for different quantities and prices.

Why Unit Testing Is Important

  • Detects programming errors early in development.
  • Verifies that methods produce expected results.
  • Reduces the need for repetitive manual testing.
  • Helps prevent existing features from breaking when code changes.
  • Improves code quality, maintainability, and reliability.
  • Supports safe refactoring and continuous integration.
  • Documents the expected behavior of application components.

2. What Is JUnit?

JUnit is a testing framework for Java that provides annotations, assertions, and test execution support. Developers use it to define test cases, execute application logic, and compare actual results with expected results.

JUnit 4 and JUnit 5 are widely recognized versions of the framework. Android projects commonly use JUnit 4 for local unit testing, while JUnit 5 can be used when the project's build configuration and Android testing setup support it.

Important JUnit Features

Feature Description
Test Methods Methods that verify a particular behavior of the application.
Annotations Identify test methods and setup or cleanup methods.
Assertions Compare expected results with actual results.
Test Execution Runs tests and reports whether they pass or fail.
Test Fixtures Prepare the objects and data required by tests.
Automation Allows tests to run repeatedly through IDEs and build tools.

3. Understanding Unit Tests and Instrumented Tests

Android applications can contain different types of tests. It is important to understand where each test runs and what it is intended to verify.

Testing Type Execution Location Typical Use
Local Unit Test Local JVM on the development machine Testing calculations, validation, business rules, and ordinary Java or Kotlin classes.
Instrumented Test Android device or emulator Testing Android framework interactions, activities, views, and device-dependent behavior.
Integration Test Depends on the test setup Verifying that multiple components work together.
UI Test Usually an emulator or physical device Checking user interactions, navigation, and screen behavior.

Local unit tests are usually faster because they do not require an Android device. Instrumented tests are useful when the behavior depends on the Android framework or a real device environment.

4. Setting Up JUnit in an Android Project

Android Studio provides support for creating and running unit tests. Most Android projects created with standard templates already include a JUnit dependency for local unit testing.

Step 1: Open the Android Project

Launch Android Studio and open the Android application project. Ensure that the project has synchronized successfully with Gradle before creating tests.

Step 2: Check the JUnit Dependency

Open the module-level Gradle configuration file, usually app/build.gradle.kts or app/build.gradle. Check whether JUnit is already included in the test dependencies.

Example using Kotlin DSL:

dependencies {
    testImplementation("junit:junit:4.13.2")
}

Example using Groovy DSL:

dependencies {
    testImplementation 'junit:junit:4.13.2'
}

Use the dependency version approved for your project and follow your organization's dependency management practices. Do not add a duplicate dependency if the project already includes it.

Step 3: Synchronize Gradle

After updating the dependencies, select the Gradle synchronization option in Android Studio. Resolve any dependency or configuration errors before writing tests.

Step 4: Locate the Test Directory

Local unit tests are normally stored under the following directory structure:

app/
├── src/
│   ├── main/
│   │   └── java/com/example/myapp/
│   │       └── Calculator.java
│   └── test/
│       └── java/com/example/myapp/
│           └── CalculatorTest.java

The package of the test class should generally correspond to the package of the class being tested. The src/test directory is used for local unit tests, while src/androidTest is commonly used for instrumented tests.

5. Understanding JUnit Annotations

Annotations tell JUnit how to recognize and execute test methods and supporting methods.

Annotation Purpose Example Use
@Test Marks a method as a test method in JUnit 4. Testing a calculation.
@Before Runs before each test method in JUnit 4. Creating a fresh object for each test.
@After Runs after each test method in JUnit 4. Cleaning up test resources.
@BeforeClass Runs once before the test methods in a JUnit 4 test class; the method is normally static. Preparing shared class-level resources.
@AfterClass Runs once after the test methods in a JUnit 4 test class; the method is normally static. Releasing shared resources.
@Ignore Temporarily skips a test in JUnit 4. Marking a test that is temporarily disabled.

These annotations belong to JUnit 4. JUnit 5 uses different lifecycle annotations, including @BeforeEach, @AfterEach, @BeforeAll, and @AfterAll.

6. Understanding Assertions in JUnit

Assertions are statements that check whether the actual result of a test matches the expected behavior. When an assertion fails, JUnit reports the test as failed and provides information to help identify the problem.

Assertion Purpose Example
assertEquals() Checks whether expected and actual values are equal. assertEquals(10, result);
assertTrue() Checks whether a condition is true. assertTrue(age >= 18);
assertFalse() Checks whether a condition is false. assertFalse(username.isEmpty());
assertNull() Checks whether a reference is null. assertNull(value);
assertNotNull() Checks whether a reference is not null. assertNotNull(user);
assertNotEquals() Checks whether two values are different. assertNotEquals(0, result);

Choose assertions that clearly express the expected behavior. For floating-point calculations, use an appropriate delta when comparing decimal values with JUnit 4.

7. Creating a Simple Calculator Class

Before writing a test, create a small class with predictable behavior. This example demonstrates how a Java class can be tested without launching an Android activity.

Calculator.java

package com.example.myapp;

public class Calculator {

    public int add(int a, int b) {
        return a + b;
    }

    public int subtract(int a, int b) {
        return a - b;
    }

    public int multiply(int a, int b) {
        return a * b;
    }

    public int divide(int a, int b) {
        if (b == 0) {
            throw new IllegalArgumentException(
                "Divisor cannot be zero"
            );
        }
        return a / b;
    }
}

The Calculator class contains four methods. Each method performs one specific operation, making it suitable for unit testing. The division method also defines an explicit behavior for a zero divisor.

8. Writing the First JUnit Test

Create a test class named CalculatorTest.java inside the corresponding package under src/test/java.

CalculatorTest.java

package com.example.myapp;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class CalculatorTest {

    @Test
    public void add_twoPositiveNumbers_returnsSum() {
        Calculator calculator = new Calculator();

        int result = calculator.add(10, 20);

        assertEquals(30, result);
    }
}

Explanation of the Test

  1. Import JUnit: The Test annotation and assertion are imported from JUnit.
  2. Create the test method: The @Test annotation tells JUnit to execute the method as a test.
  3. Create the object: A new Calculator instance is created.
  4. Execute the method: The test calls add(10, 20).
  5. Verify the result: The assertion checks whether the returned value is 30.

If the actual result is 30, the test passes. If the result differs, the test fails and reports the expected and actual values.

9. Testing Multiple Methods

A test class can contain several test methods. Each test should focus on a specific behavior so that failures are easy to understand.

package com.example.myapp;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class CalculatorTest {

    @Test
    public void add_returnsSum() {
        Calculator calculator = new Calculator();
        assertEquals(15, calculator.add(10, 5));
    }

    @Test
    public void subtract_returnsDifference() {
        Calculator calculator = new Calculator();
        assertEquals(5, calculator.subtract(10, 5));
    }

    @Test
    public void multiply_returnsProduct() {
        Calculator calculator = new Calculator();
        assertEquals(50, calculator.multiply(10, 5));
    }

    @Test
    public void divide_returnsQuotient() {
        Calculator calculator = new Calculator();
        assertEquals(2, calculator.divide(10, 5));
    }
}

Each method tests a different operation. If multiplication fails, the developer can investigate that behavior without having to interpret one large test that combines every operation.

10. Testing Exceptions

Some methods should reject invalid input rather than return a normal result. Unit tests should verify that the expected exception is thrown when such input is provided.

For the calculator example, division by zero should produce an IllegalArgumentException.

import org.junit.Test;

public class CalculatorExceptionTest {

    @Test(expected = IllegalArgumentException.class)
    public void divide_byZero_throwsException() {
        Calculator calculator = new Calculator();
        calculator.divide(10, 0);
    }
}

In JUnit 4, the expected attribute checks that the specified exception occurs during the test. If the exception is not thrown, the test fails. Keep the operation that should throw the exception narrowly scoped, so unrelated exceptions do not accidentally satisfy the test.

11. Using @Before to Prepare Test Data

When multiple test methods require the same setup, JUnit 4 provides the @Before annotation. Its method runs before each test method, helping tests start with a clean and predictable state.

package com.example.myapp;

import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class CalculatorSetupTest {

    private Calculator calculator;

    @Before
    public void setUp() {
        calculator = new Calculator();
    }

    @Test
    public void add_returnsCorrectResult() {
        assertEquals(12, calculator.add(7, 5));
    }

    @Test
    public void subtract_returnsCorrectResult() {
        assertEquals(2, calculator.subtract(7, 5));
    }
}

The setUp() method creates a calculator before every test. This avoids repeating object creation in every method and helps keep the tests organized.

12. Following the Arrange-Act-Assert Pattern

The Arrange-Act-Assert (AAA) pattern is a common structure for writing readable unit tests.

Stage Description Calculator Example
Arrange Prepare the objects and input data. Create a calculator and select the input numbers.
Act Execute the behavior being tested. Call the addition method.
Assert Verify the expected result. Check that the returned value equals 15.
@Test
public void add_fiveAndTen_returnsFifteen() {
    // Arrange
    Calculator calculator = new Calculator();
    int firstNumber = 5;
    int secondNumber = 10;

    // Act
    int result = calculator.add(firstNumber, secondNumber);

    // Assert
    assertEquals(15, result);
}

Using this pattern makes test methods easier to read, debug, and maintain.

13. Testing Business Logic in Android Applications

Many Android applications contain business logic that can be tested independently of the interface. Examples include calculating discounts, validating registration details, checking password rules, and computing order totals.

Consider a discount calculator used by a shopping application.

DiscountCalculator.java

package com.example.myapp;

public class DiscountCalculator {

    public double calculateDiscount(
            double price, double discountPercent) {

        if (price < 0) {
            throw new IllegalArgumentException(
                "Price cannot be negative"
            );
        }

        if (discountPercent < 0 ||
                discountPercent > 100) {
            throw new IllegalArgumentException(
                "Discount must be between 0 and 100"
            );
        }

        return price * discountPercent / 100.0;
    }

    public double calculateFinalPrice(
            double price, double discountPercent) {

        return price - calculateDiscount(
            price, discountPercent
        );
    }
}

DiscountCalculatorTest.java

package com.example.myapp;

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class DiscountCalculatorTest {

    @Test
    public void calculateDiscount_returnsCorrectAmount() {
        DiscountCalculator calculator =
            new DiscountCalculator();

        double discount =
            calculator.calculateDiscount(1000, 10);

        assertEquals(100.0, discount, 0.001);
    }

    @Test
    public void calculateFinalPrice_returnsReducedPrice() {
        DiscountCalculator calculator =
            new DiscountCalculator();

        double finalPrice =
            calculator.calculateFinalPrice(1000, 10);

        assertEquals(900.0, finalPrice, 0.001);
    }
}

These tests verify the discount amount and final price. The third argument in assertEquals() specifies the permitted difference when comparing floating-point values.

14. Testing Edge Cases

Edge cases are inputs that occur at the boundaries of acceptable values or in unusual situations. Testing them helps developers discover problems that ordinary inputs may not reveal.

For a discount calculator, useful cases include:

  • A price of zero.
  • A discount percentage of zero.
  • A discount percentage of 100.
  • A negative price.
  • A discount percentage below zero.
  • A discount percentage above 100.
  • Very large input values that might cause arithmetic problems.

For example, the following test verifies that a discount of 100 percent produces a final price of zero.

@Test
public void calculateFinalPrice_withFullDiscount_returnsZero() {
    DiscountCalculator calculator =
        new DiscountCalculator();

    double result =
        calculator.calculateFinalPrice(500, 100);

    assertEquals(0.0, result, 0.001);
}

Good test coverage includes normal values, boundaries, invalid input, and important failure conditions. The goal is to verify meaningful behavior rather than simply increase the number of tests.

15. Testing Input Validation

Input validation prevents incorrect or incomplete data from entering an application. Android applications often validate usernames, email addresses, passwords, phone numbers, and form fields.

Example of a simple username validator:

package com.example.myapp;

public class UsernameValidator {

    public boolean isValid(String username) {
        return username != null
            && username.trim().length() >= 3;
    }
}

Corresponding JUnit tests:

package com.example.myapp;

import org.junit.Test;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;

public class UsernameValidatorTest {

    private final UsernameValidator validator =
        new UsernameValidator();

    @Test
    public void isValid_withLongEnoughUsername_returnsTrue() {
        assertTrue(validator.isValid("Manish"));
    }

    @Test
    public void isValid_withShortUsername_returnsFalse() {
        assertFalse(validator.isValid("AB"));
    }

    @Test
    public void isValid_withNullUsername_returnsFalse() {
        assertFalse(validator.isValid(null));
    }

    @Test
    public void isValid_withWhitespaceOnly_returnsFalse() {
        assertFalse(validator.isValid("   "));
    }
}

These tests verify valid input, short input, null input, and whitespace-only input. The example demonstrates basic validation only; real applications may need additional rules depending on product requirements.

16. Mocking Dependencies in Unit Tests

In larger Android applications, a class may depend on a database, network service, repository, or another component. Testing the class against a real external service can make tests slow, unreliable, or difficult to repeat.

Mocking replaces a real dependency with a controlled test object. A mock can return predefined results or verify whether a method was called.

For example, a login service may depend on a user repository. A mocked repository allows the developer to test the login logic without connecting to a real database.

Mockito is a commonly used Java mocking framework that can work with JUnit. Its compatibility and configuration should be checked against the project's Java, Android, and testing requirements.

// Example dependency interface
public interface UserRepository {
    boolean userExists(String username);
}

// Example service under test
public class LoginService {

    private final UserRepository repository;

    public LoginService(UserRepository repository) {
        this.repository = repository;
    }

    public boolean canLogin(String username) {
        return username != null
            && !username.trim().isEmpty()
            && repository.userExists(username);
    }
}

Example using Mockito with JUnit 4:

import org.junit.Test;

import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class LoginServiceTest {

    @Test
    public void canLogin_existingUser_returnsTrue() {
        UserRepository repository =
            mock(UserRepository.class);

        when(repository.userExists("alex"))
            .thenReturn(true);

        LoginService service =
            new LoginService(repository);

        assertTrue(service.canLogin("alex"));
    }
}

To run this example, add a compatible Mockito test dependency to the project. The test verifies the service's behavior using a controlled repository response rather than a live data source.

17. Running JUnit Tests in Android Studio

After writing the test class, execute the tests to verify that they pass.

  1. Open the test file in Android Studio.
  2. Locate the green run icon beside the test method or class declaration.
  3. Select the run option for the individual test or the entire test class.
  4. Wait for the test runner to finish execution.
  5. Review the results in the Run window.
  6. Open any failed test to inspect its assertion message and stack trace.
  7. Correct the implementation or test as necessary, then run the tests again.

For local unit tests, Android Studio normally runs the tests on the local JVM. An emulator or physical Android device is not usually required for pure Java logic.

18. Running Unit Tests Using Gradle

Gradle can execute unit tests from the command line, making testing useful for automated build pipelines and continuous integration.

On Windows, use the Gradle wrapper command:

gradlew.bat testDebugUnitTest

On macOS or Linux, use:

./gradlew testDebugUnitTest

The exact task name can vary according to the Android project, build variants, and Gradle configuration. You can inspect available tasks or use the task corresponding to the variant you want to test.

Gradle reports failures in the command output and typically generates HTML test reports under a directory similar to:

app/build/reports/tests/testDebugUnitTest/index.html

Use the report to review which tests passed, failed, or were skipped and to inspect failure details.

19. Common JUnit Testing Errors and Solutions

Problem Possible Cause Solution
JUnit classes cannot be found. Missing dependency or Gradle synchronization issue. Check the test dependency and synchronize Gradle.
Tests do not appear in Android Studio. Incorrect source directory or missing test annotation. Check the src/test directory, package, imports, and test configuration.
Assertion failure. The actual result differs from the expected result. Inspect the method implementation, test inputs, and assertion.
NullPointerException. An object or dependency was not initialized. Review setup methods, constructors, and mock configuration.
Tests fail because of Android APIs. A local test directly uses Android framework behavior unavailable in the local JVM. Separate business logic from Android-specific code or use an appropriate instrumented test.
Tests pass individually but fail together. Tests may share mutable state or depend on execution order. Make tests independent and reset state before each test.
Unexpected exception in a test. Invalid input or setup may trigger a different exception than expected. Check the stack trace and isolate the operation being tested.
Tests are slow or unreliable. Tests depend on live APIs, databases, or external resources. Use controlled test doubles and isolate external dependencies where appropriate.

20. Best Practices for Writing JUnit Tests

  • Use descriptive test names: Explain the condition and expected behavior, such as divide_byZero_throwsException.
  • Test one behavior at a time: Keep each test focused and easy to diagnose.
  • Follow Arrange-Act-Assert: Separate preparation, execution, and verification.
  • Test boundary conditions: Include zero, minimum, maximum, null, empty, and invalid inputs where relevant.
  • Keep tests independent: A test should not rely on another test running first.
  • Avoid unnecessary complexity: Prefer simple test data and straightforward assertions.
  • Use mocks carefully: Mock external dependencies when needed, but do not replace the behavior you actually want to test.
  • Test observable behavior: Focus on the expected output or side effect rather than internal implementation details.
  • Run tests regularly: Execute the relevant suite after code changes and before merging changes.
  • Include tests in CI: Automate unit test execution during builds to detect regressions early.
  • Do not ignore failures: Investigate failed tests rather than changing assertions merely to make them pass.

21. Unit Testing and Test-Driven Development

Test-Driven Development (TDD) is a development approach in which a developer writes a test before implementing the corresponding functionality. The process commonly follows three stages.

  1. Red: Write a test for the desired behavior and confirm that it fails because the functionality is missing or incorrect.
  2. Green: Implement the minimum code needed to make the test pass.
  3. Refactor: Improve the code structure while keeping the tests passing.

For example, a developer may first write a test requiring a username validator to reject blank strings. The developer then implements the validation logic and finally refactors the code without changing the expected behavior.

TDD encourages small development steps, quick feedback, and clear definitions of expected behavior. It is useful for many business logic components in Android applications.

22. Practical Exercise: Create and Test a Grade Calculator

Build a small Java class that converts a student's marks into a grade. Write JUnit tests to verify normal values and boundary conditions.

Requirements

  • Accept marks between 0 and 100.
  • Return grade A for marks of 90 or higher.
  • Return grade B for marks of 75 to 89.
  • Return grade C for marks of 60 to 74.
  • Return grade D for marks of 40 to 59.
  • Return grade F for marks below 40.
  • Reject values below 0 or above 100 with an exception.

Suggested Test Cases

Input Marks Expected Result Purpose
95 A High score.
90 A Upper-grade boundary.
75 B Grade B boundary.
60 C Grade C boundary.
40 D Passing boundary.
25 F Failing score.
-1 Exception Invalid lower boundary.
101 Exception Invalid upper boundary.

Write the calculator class, create a separate JUnit test class, run all test cases, and correct any failures. Add further tests for any other conditions specified by your requirements.

23. Interview Questions and Answers

Q1. What is JUnit?

JUnit is a Java testing framework used to write and execute automated unit tests. It provides annotations, assertions, and test lifecycle support.

Q2. What is unit testing?

Unit testing verifies that an individual method, class, or other small unit of software behaves as expected.

Q3. What is the purpose of the @Test annotation?

In JUnit 4, @Test marks a method as a test method that the test runner can execute.

Q4. What is the difference between @Before and @BeforeClass?

@Before runs before each test method in JUnit 4. @BeforeClass runs once before the test methods in a test class and normally requires a static method.

Q5. What is an assertion?

An assertion checks whether a result or condition matches the expected behavior. If the assertion fails, JUnit reports the test as failed.

Q6. Where are local unit tests stored in an Android project?

They are normally stored in the module's src/test directory. Instrumented tests are normally stored in src/androidTest.

Q7. What is the difference between local and instrumented tests?

Local tests generally run on the development machine's JVM. Instrumented tests run on an Android device or emulator and can interact with Android framework components.

Q8. Why should developers test edge cases?

Edge cases reveal errors involving boundaries, invalid inputs, empty values, and unusual conditions that may not occur in ordinary scenarios.

Q9. What is mocking?

Mocking uses controlled test objects to simulate dependencies. It allows a class to be tested without requiring real external services or databases.

Q10. What is the Arrange-Act-Assert pattern?

It organizes tests into three stages: preparing the test conditions, executing the behavior, and verifying the result.

Q11. Can JUnit test Android UI components?

JUnit is used in Android testing, but tests that require real Android UI or framework behavior generally need an instrumented testing setup and suitable Android testing libraries.

Q12. What is test-driven development?

TDD is an approach in which developers write a failing test first, implement the required functionality, and then refactor while keeping the tests passing.

24. Quick Revision Checklist

  • Understand the purpose of unit testing.
  • Know the difference between local and instrumented Android tests.
  • Configure the JUnit dependency in Gradle.
  • Create test classes under the correct source directory.
  • Use JUnit annotations appropriately.
  • Verify results using assertions.
  • Test normal inputs, invalid inputs, and edge cases.
  • Use setup methods to prepare test data when helpful.
  • Mock external dependencies when appropriate.
  • Run tests through Android Studio or Gradle.
  • Investigate test failures and maintain independent test cases.

25. Conclusion

JUnit is an important tool for building reliable Android applications because it allows developers to verify application logic automatically. By writing focused test methods, using assertions, checking edge cases, and testing business logic independently of the user interface, developers can detect defects earlier and make changes with greater confidence.

Practicing JUnit with calculators, input validators, grade calculators, and service classes provides a strong foundation for testing real Android application features. As projects become more complex, JUnit can be combined with mocking frameworks, instrumented tests, and continuous integration to establish a more complete testing strategy.


Learn Android App Development with JustAcademy

Explore Android application development concepts and build practical programming skills through structured learning resources.

Course: Explore the Android App Development Course

Free Demo: Register for a Course Demo

Official Website: Visit JustAcademy

Previous Next
whatsapp